//
//  Copyright (C) 2002 HorizonLive.com, Inc.  All Rights Reserved.
//  Copyright (C) 2008 Wimba, Inc.  All Rights Reserved.
//
//  This is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This software is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this software; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
//  USA.
//

//
// FbsInputStream.java
//

package name.elftesting.rfbplayer;

import java.io.*;
import java.util.*;

class FbsInputStream extends InputStream {

	protected InputStream in;
	protected long startTime;
	protected long timeOffset;
	protected long seekOffset;
	protected boolean farSeeking;
	protected boolean paused;
	protected boolean isQuitting = false;
	protected double playbackSpeed;

	protected byte[] buffer;
	protected int bufferSize;
	protected int bufferPos;

	/** The number of bytes to skip in the beginning of the next data block. */
	protected long nextBlockOffset;

	protected Observer obs;

	/**
	 * Construct FbsInputStream object based on the given InputStream,
	 * positioned at the very beginning of the corresponding FBS file. This
	 * constructor reads and checks FBS file signature which would look like
	 * "FBS 001.000\n", but any decimal number is allowed after the dot.
	 * 
	 * @param in
	 *            the InputStream object that will be used as a base for this
	 *            new FbsInputStream instance. It should be positioned at the
	 *            very beginning of the corresponding FBS file, so that first 12
	 *            bytes read from the stream should form FBS file signature.
	 * @throws java.io.IOException
	 *             thrown on read error or on incorrect FBS file signature.
	 */
	FbsInputStream(InputStream in) throws IOException {
		this(in, 0, null, 0);

		byte[] b = new byte[12];
		readFully(b);

		if (b[0] != 'F' || b[1] != 'B' || b[2] != 'S' || b[3] != ' '
				|| b[4] != '0' || b[5] != '0' || b[6] != '1' || b[7] != '.'
				|| b[8] < '0' || b[8] > '9' || b[9] < '0' || b[9] > '9'
				|| b[10] < '0' || b[10] > '9' || b[11] != '\n') {
			throw new IOException("Incorrect FBS file signature");
		}
	}

	/**
	 * Construct FbsInputStream object based on the given byte array and
	 * continued in the specified InputStream. Arbitrary position in the FBS
	 * file is allowed.
	 * 
	 * @param in
	 *            the input stream for reading future data, after
	 *            <code>buffer</code> will be exhausted. The stream should be
	 *            positioned at any data block boundary (byte counter should
	 *            follow in next four bytes).
	 * @param timeOffset
	 *            time position corresponding the the data block provided in
	 *            <code>buffer</code>.
	 * @param buffer
	 *            the data block that will be treated as the beginning of this
	 *            FBS data stream. This byte array is not copied into the new
	 *            object so it should not be altered by the caller afterwards.
	 * @param nextBlockOffset
	 *            the number of bytes that should be skipped in first data block
	 *            read from <code>in</code>.
	 */
	FbsInputStream(InputStream in, long timeOffset, byte[] buffer,
			long nextBlockOffset) {

		this.in = in;
		startTime = System.currentTimeMillis() - timeOffset;
		this.timeOffset = timeOffset;
		seekOffset = -1;
		farSeeking = false;
		paused = false;
		playbackSpeed = 1.0;

		this.buffer = buffer;
		bufferSize = (buffer != null) ? buffer.length : 0;
		bufferPos = 0;

		this.nextBlockOffset = nextBlockOffset;
	}

	// Force stream to finish any wait.
	public void quit() {
		isQuitting = true;
		synchronized (this) {
			notify();
		}
	}

	//
	// Basic methods overriding InputStream's methods.
	//
	public int read() throws IOException {
		while (bufferSize == 0) {
			if (!fillBuffer())
				return -1;
		}
		bufferSize--;
		return buffer[bufferPos++] & 0xFF;
	}

	public int available() throws IOException {
		// FIXME: This will work incorrectly if our caller will wait until
		// some amount of data is available when the buffer contains less
		// data than then that. Current implementation never reads more
		// data until the buffer is fully exhausted.
		return bufferSize;
	}

	public synchronized void close() throws IOException {
		if (in != null)
			in.close();
		in = null;
		startTime = -1;
		timeOffset = 0;
		seekOffset = -1;
		farSeeking = false;
		paused = false;
		playbackSpeed = 1.0;

		buffer = null;
		bufferSize = 0;
		bufferPos = 0;

		nextBlockOffset = 0;
		obs = null;
	}

	//
	// Methods providing additional functionality.
	//
	public synchronized long getTimeOffset() {
		long off = Math.max(seekOffset, timeOffset);
		return (long) (off * playbackSpeed);
	}

	public synchronized void setTimeOffset(long pos, boolean allowJump) {
		seekOffset = (long) (pos / playbackSpeed);
		if (allowJump) {
			long minJumpForwardOffset = timeOffset
					+ (long) (10000 / playbackSpeed);
			if (seekOffset < timeOffset || seekOffset > minJumpForwardOffset) {
				farSeeking = true;
			}
		}
		notify();
	}

	public synchronized void setSpeed(double newSpeed) {
		long newOffset = (long) (timeOffset * playbackSpeed / newSpeed);
		startTime += timeOffset - newOffset;
		timeOffset = newOffset;
		if (isSeeking()) {
			seekOffset = (long) (seekOffset * playbackSpeed / newSpeed);
		}
		playbackSpeed = newSpeed;
	}

	public boolean isSeeking() {
		return (seekOffset >= 0);
	}

	public long getSeekOffset() {
		return (long) (seekOffset * playbackSpeed);
	}

	public boolean isPaused() {
		return paused;
	}

	public synchronized void pausePlayback() {
		paused = true;
		notify();
	}

	public synchronized void resumePlayback() {
		paused = false;
		startTime = System.currentTimeMillis() - timeOffset;
		notify();
	}

	public void addObserver(Observer target) {
		obs = target;
	}

	//
	// Methods for internal use.
	//
	private synchronized boolean fillBuffer() throws IOException {
		// The reading thread should be interrupted on far seeking.
		if (farSeeking)
			throw new EOFException("[JUMP]");

		// Just wait unless we are performing playback OR seeking.
		waitWhilePaused();

		if (!readDataBlock()) {
			return false;
		}

		if (seekOffset >= 0) {
			if (timeOffset >= seekOffset) {
				startTime = System.currentTimeMillis() - seekOffset;
				seekOffset = -1;
			} else {
				return true;
			}
		}

		while (!isQuitting) {
			long timeDiff = startTime + timeOffset - System.currentTimeMillis();
			if (timeDiff <= 0) {
				break;
			}
			try {
				wait(timeDiff);
			} catch (InterruptedException e) {
			}
			waitWhilePaused();
		}

		return true;
	}

	/**
	 * Read FBS data block into the buffer. If {@link #nextBlockOffset} is not
	 * zero, that number of bytes will be skipped in the beginning of the data
	 * block.
	 * 
	 * @return true on success, false if end of file was reached.
	 * @throws java.io.IOException
	 *             can be thrown while reading from the underlying input stream,
	 *             or as a result of bad FBS file data.
	 */
	private boolean readDataBlock() throws IOException {
		// Read byte counter, check for EOF condition.
		long readResult = readUnsigned32();
		if (readResult < 0) {
			return false;
		}

		bufferSize = (int) readResult;
		int alignedSize = (bufferSize + 3) & 0xFFFFFFFC;

		if (nextBlockOffset > 0) {
			in.skip(nextBlockOffset);
			bufferSize -= nextBlockOffset;
			alignedSize -= nextBlockOffset;
			nextBlockOffset = 0;
		}

		if (bufferSize >= 0) {
			buffer = new byte[alignedSize];
			readFully(buffer);
			bufferPos = 0;
			timeOffset = (long) (readUnsigned32() / playbackSpeed);
		}

		if (bufferSize < 0 || timeOffset < 0 || bufferPos >= bufferSize) {
			buffer = null;
			bufferSize = 0;
			bufferPos = 0;
			throw new IOException("Invalid FBS file data");
		}

		return true;
	}

	//
	// In paused mode, wait for external notification on this object.
	//
	private void waitWhilePaused() {
		while (paused && !isSeeking() && !isQuitting) {
			synchronized (this) {
				try {
					// Note: we call Observer.update(Observable,Object) method
					// directly instead of maintaining an Observable object.
					obs.update(null, null);
					wait();
				} catch (InterruptedException e) {
				}
			}
		}
	}

	private long readUnsigned32() throws IOException {
		byte[] buf = new byte[4];
		if (!readFully(buf))
			return -1;

		return ((long) (buf[0] & 0xFF) << 24 | (buf[1] & 0xFF) << 16
				| (buf[2] & 0xFF) << 8 | (buf[3] & 0xFF));
	}

	private boolean readFully(byte[] b) throws IOException {
		int off = 0;
		int len = b.length;

		while (off != len) {
			int count = in.read(b, off, len - off);
			if (count < 0) {
				return false;
			}
			off += count;
		}

		return true;
	}

}
